import sys
import inspect


class CliOptsError(Exception):
    pass
class RepeatedArgument(CliOptsError):
    pass
class WrongNumberOfArguments(CliOptsError):
    pass
class ArgumentTypeError(CliOptsError):
    pass
class InvalidTailArg(CliOptsError):
    pass
class TooManyTailArgs(CliOptsError):
    pass
class TooFewTailArgs(CliOptsError):
    pass
class InvalidSwitch(CliOptsError):
    pass
class MissingMandatorySwitches(CliOptsError):
    pass



def switch(short_name = None, long_name = None, args = [], mandatory = False, 
           allow_repeat = False, quit_after = False):
    if short_name is None and long_name is None:
        raise ValueError("at least short_name or long_name must be given")
    if short_name and len(short_name) != 1:
        raise ValueError("short_name can only be one character long")
    def wrapper(func):
        already_invoked = [False]
        def wrapper2(self, switchname, argsiter):
            if already_invoked[0] and not allow_repeat:
                raise RepeatedArgument("%s cannot be given multiple times" 
                    % (switchname,))
            already_invoked[0] = True
            fargs = []
            cnt = 0
            for t in args:
                try:
                    a = argsiter.next()
                    cnt += 1
                except StopIteration:
                    raise WrongNumberOfArguments("%s: expected %d, found %d" 
                        % (switchname, len(args), cnt))
                try:
                    fargs.append(t(a))
                except:
                    raise ArgumentTypeError("%s: %r cannot be converted to %s" 
                        % (switchname, a, t))
            func(self, *fargs)
            if quit_after:
                sys.exit()
        argnames, varargs, kwargs, defaults = inspect.getargspec(func)
        if argnames[0] == "self":
            argnames.pop(0)
        wrapper2._cli_switch_function = True
        wrapper2._short_name = short_name
        wrapper2._long_name = long_name
        wrapper2._mandatory = mandatory
        wrapper2.__doc__ = func.__doc__
        wrapper2.__name__ = func.__name__
        wrapper2._argtypes = args
        wrapper2._argnames = argnames
        return wrapper2
    return wrapper


class CliOpts(object):
    program_name = "program"
    program_version = "<unknown>"
    short_prefix = "-"
    long_prefix = "--"
    min_tail_args = 0
    max_tail_args = 0
    usage = "[OPTIONS] tailargs"
    
    def __init__(self):
        self._switch_table = {}
        self._mandatories = set()
        for name in dir(self):
            attr = getattr(self, name, None)
            if getattr(attr, "_cli_switch_function", False):
                if attr._short_name:
                    self._switch_table[attr._short_name] = attr
                if attr._long_name:
                    self._switch_table[attr._long_name] = attr
                if attr._mandatory:
                    self._mandatories.add(attr)
    
    def parse(self, args):
        args = iter(args)
        tail = []
        invoked = set()
        for a in args:
            name = self.getargname(a)
            if name is not None:
                if name not in self._switch_table:
                    raise InvalidSwitch(a)
                if tail:
                    raise InvalidTailArg(
                        "tail args must come last, found %r before %r" 
                        % (tail, a))
                func = self._switch_table[name]
                func(name, args)
                invoked.add(func)
            else:
                if self.max_tail_args > 0:
                    if len(tail) > self.max_tail_args:
                        raise TooManyTailArgs("expected up to %d, found %s" 
                            % (self.max_tail_args, tail + [a]))
                    tail.append(a)
                else:
                    raise InvalidTailArg(
                        "program does not accept tail args, found %r" % (a,))
        if len(tail) < self.min_tail_args:
            raise TooFewTailArgs("expected at least %d, found %r" 
                % (self.min_tail_args, tail))
        uninvoked = self._mandatories - invoked
        if uninvoked:
            raise MissingMandatorySwitches(uninvoked)
        return tail
    
    def run(self, args = None):
        if args is None:
            args = sys.argv[1:]
        
        try:
            tail = self.parse(args)
        except CliOptsError, ex:
            print "Error parsing command line arguments:"
            print "    %r" % (ex,)
            print
            self.show_help()
            retcode = False
        else:
            retcode = self.main(*tail)
        
        if retcode is None:
            sys.exit(0)
        if isinstance(retcode, bool):
            if retcode:
                sys.exit(0)
            else:
                sys.exit(1)
        else:
            sys.exit(retcode)
    
    def getargname(self, arg):
        if arg.startswith(self.long_prefix):
            return arg[len(self.long_prefix):]
        if arg.startswith(self.short_prefix):
            return arg[len(self.short_prefix):]
        return None
    
    def main(self, tailargs):
        pass
    
    def show_help(self):
        if self.__doc__:
            print self.__doc__
        print "Usage: %s %s" % (self.program_name, self.usage)
        if not self._switch_table:
            print "(No options accepted)"
            return
        
        print "Options:"
        funcs = list(set(self._switch_table.values()))
        funcs.sort(key = lambda f: f.__name__)
        for func in funcs:
            if func.__doc__ is None:
                doc = func.__name__
            else:
                doc = inspect.getdoc(func).replace("\n", "")
            names = []
            if func._short_name:
                names.append(self.short_prefix + func._short_name)
            if func._long_name:
                names.append(self.long_prefix + func._long_name)
            names = ", ".join(names)
            names += " " + " ".join(func._argnames)
            names = names.strip()
            w = 80-35
            lines = [doc[i:i+w] for i in range(0, len(doc), w)]
            aligned = ("\n"+" "*34).join(lines)
            print "   %-30s %s" % (names, aligned)
    
    @switch("h", "help", allow_repeat = True, quit_after = True)
    def help(self):
        """displays this help message and quits"""
        self.show_help()
    
    @switch("v", "version", quit_after = True)
    def version(self):
        """displays the programs's version and quits"""
        print "%s: %s" % (self.program_name, self.program_version)

























